/*******************************************************************************
 * Copyright (c) 2009 Kevin Bierhoff and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Kevin Bierhoff - initial API and implementation
 *******************************************************************************/
package com.googlecode.twelfdevtools.editor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.DefaultCharacterPairMatcher;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.ICharacterPairMatcher;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;

import antlr.RecognitionException;
import antlr.TokenStreamException;

import com.googlecode.twelfdevtools.TwelfActivator;
import com.googlecode.twelfdevtools.editor.OccurrencesFinder.Occurrence;
import com.googlecode.twelfdevtools.model.IElfFile;
import com.googlecode.twelfdevtools.model.ITwelfConstant;
import com.googlecode.twelfdevtools.model.ITwelfModelElement;
import com.googlecode.twelfdevtools.model.ITwelfNamedElement;
import com.googlecode.twelfdevtools.model.TwelfModel;
import com.googlecode.twelfdevtools.preferences.PreferenceConstants;
import com.googlecode.twelfdevtools.twelfast.ITwelfDeclaration;
import com.googlecode.twelfdevtools.twelfast.TwelfAST;
import com.googlecode.twelfdevtools.views.ElfFileOutline;


public class TwelfEditor extends TextEditor {
	
	private final class OccurrenceMarkerJob extends Job {
		
		private final List<Occurrence> occurrences;

		public OccurrenceMarkerJob(List<Occurrence> occurrences) {
			super("Twelf occurrence marker");
			this.occurrences = occurrences;
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			if (monitor.isCanceled())
				return Status.CANCEL_STATUS;
			Map<Annotation, Position> annos = new HashMap<Annotation, Position>();
			for (Occurrence o : occurrences) {
				if (monitor.isCanceled())
					return Status.CANCEL_STATUS;
				String type = o.isDefinition ? 
						OCCURRENCE_DEFINITION_MARKER_TYPE :
						OCCURRENCE_USE_MARKER_TYPE;
				String desc = o.isDefinition ? "Definition" : "Use";
				Annotation a = new Annotation(type, false /* transient */, desc);
				if (o.offset >= 0 && o.length > 0) {
					Position p = new Position(o.offset, o.length);
					annos.put(a, p);
				} // otherwise there was an error getting the position
			}
			if (monitor.isCanceled())
				return Status.CANCEL_STATUS;

			IAnnotationModel annotationModel = getSourceViewer().getAnnotationModel();
			if (monitor.isCanceled())
				return Status.CANCEL_STATUS;
			synchronized (getLockObject(annotationModel)) {
				if (monitor.isCanceled())
					// check while locked to allow cancellation "for sure" 
					// by grabbing the lock before canceling
					return Status.CANCEL_STATUS;

				if (annotationModel instanceof IAnnotationModelExtension) {
					((IAnnotationModelExtension)annotationModel).replaceAnnotations(
							occurrenceAnnos, annos);
				} else {
					System.err.println("Replacing annotations not supported :(");
				}
				// remember these annotations so we can remove them next time
				occurrenceAnnos = annos.keySet().toArray(new Annotation[0]);
			}
			
			return Status.OK_STATUS;
		}

	}
	
	public static final char[] TWELF_BRACKETS = 
			{ '(', ')', '{', '}', '[', ']' };

	// use JDT marker types since they are available to all editors
	private static final String OCCURRENCE_DEFINITION_MARKER_TYPE =
		"org.eclipse.jdt.ui.occurrences.write";
	private static final String OCCURRENCE_USE_MARKER_TYPE =
		"org.eclipse.jdt.ui.occurrences";

	private ColorManager colorManager;
	private ElfFileOutline outline;
	private ICharacterPairMatcher bracketMatcher =
		new DefaultCharacterPairMatcher(TWELF_BRACKETS);

	// state for marking variable occurrences
	/** current occurrence annotations, so we can remove them when selection changes */
	private Annotation[] occurrenceAnnos;
	/** Occurrence marker job, so we can cancel */
	private volatile OccurrenceMarkerJob occurrenceMarkerJob;

	private boolean markOccurrences;

	public TwelfEditor() {
		super();
		colorManager = new ColorManager();
		setSourceViewerConfiguration(new TwelfEditorConfiguration(this, colorManager));
		setDocumentProvider(new TwelfDocumentProvider());
	}
	
	public IElfFile getFile() {
		return getEditedFile(this);
	}
	
	@Override
	public void createPartControl(Composite parent) {
		super.createPartControl(parent);
		
		// install post selection listener to update occurrence marker
		markOccurrences = getPreferenceStore().getBoolean(PreferenceConstants.P_HIGHLIGHT_OCCURRENCES);
		// TODO could make this cleaner by only registering the listener if markOccurrences set
		getEditorSite().getPage().addPostSelectionListener(getEditorSite().getId(), new ISelectionListener() {
			public void selectionChanged(IWorkbenchPart part,
					ISelection selection) {
				if (markOccurrences && TwelfEditor.this == part && selection instanceof ITextSelection) {
					doSelectionChanged((ITextSelection) selection);
				}
			}
		});
	}
	
	@Override
	public void dispose() {
		colorManager.dispose();
		super.dispose();
	}
	
	@Override
	public Object getAdapter(Class adapter) {
		if(IContentOutlinePage.class.equals(adapter)) {
			if(outline == null) {
				outline = new ElfFileOutline(getSourceViewer());				
				outline.addSelectionChangedListener(new ISelectionChangedListener() {

					public void selectionChanged(SelectionChangedEvent event) {
				       // if the selection is empty clear the label do nothing
				       if(event.getSelection().isEmpty()) {
				           return;
				       }
				       // move editor to selected constant
				       if(event.getSelection() instanceof IStructuredSelection) {
				           IStructuredSelection selection = (IStructuredSelection)event.getSelection();
				           for (Iterator<?> iterator = selection.iterator(); iterator.hasNext();) {
				        	   ITwelfModelElement sel = (ITwelfModelElement) iterator.next();
				        	   if(sel instanceof ITwelfConstant) {
				        		   ITwelfConstant c = (ITwelfConstant) sel;
				        		   IRegion r = c.getIdentifierRange();
				        		   TwelfEditor.this.selectAndReveal(r.getOffset(), r.getLength());
				        		   return;
				        	   }
				           }
				       }
					}
					
				});
			}
			return outline;
		}
		return super.getAdapter(adapter);
	}

	@Override
	protected void initializeEditor() {
		super.initializeEditor();
		// TODO may have to revert to what JavaEditor does: set in doSetInput
		setPreferenceStore(createCombinedPreferenceStore());
	}

	@Override
	protected void configureSourceViewerDecorationSupport(
			SourceViewerDecorationSupport support) {
		// plug in bracket matcher for Twelf brackets ( { [ ] } )
		support.setCharacterPairMatcher(bracketMatcher);
		support.setMatchingCharacterPainterPreferenceKeys(
				PreferenceConstants.P_MATCH_BRACKETS, 
				PreferenceConstants.P_MATCHING_BRACKETS_COLOR);
		super.configureSourceViewerDecorationSupport(support);
	}
	
	// inspired by JavaEditor#doSetInput(IEditorInput)
	// replaced "internalDoSetInput" by simple super call.
	// install preference store once in initializeEditor() for now
//	@Override
//	protected void doSetInput(IEditorInput input) throws CoreException {
//		ISourceViewer sourceViewer= getSourceViewer();
//		if (!(sourceViewer instanceof ISourceViewerExtension2)) {
//			setPreferenceStore(createCombinedPreferenceStore());
//			super.doSetInput(input);
//			return;
//		}
//
//		// uninstall & unregister preference store listener
//		getSourceViewerDecorationSupport(sourceViewer).uninstall();
//		((ISourceViewerExtension2)sourceViewer).unconfigure();
//
//		setPreferenceStore(createCombinedPreferenceStore());
//
//		// install & register preference store listener
//		sourceViewer.configure(getSourceViewerConfiguration());
//		getSourceViewerDecorationSupport(sourceViewer).install(getPreferenceStore());
//
//		super.doSetInput(input);
//	}

	@Override
	protected void handlePreferenceStoreChanged(PropertyChangeEvent event) {
		
		String property = event.getProperty();
		try {
			if (event.getNewValue() == null)
				return;
			if (PreferenceConstants.P_HIGHLIGHT_OCCURRENCES.equals(property)) {
				boolean newValue;
				if (event.getNewValue() instanceof Boolean)
					newValue = (Boolean) event.getNewValue();
				else
					newValue = Boolean.valueOf(event.getNewValue().toString());
				markOccurrences = newValue;
				if (newValue == false && occurrenceAnnos != null) {
					IAnnotationModel annotationModel = getSourceViewer().getAnnotationModel();
					synchronized (getLockObject(annotationModel)) {
						occurrenceMarkerJob.cancel();
						if (annotationModel instanceof IAnnotationModelExtension) {
							((IAnnotationModelExtension) annotationModel).replaceAnnotations(occurrenceAnnos, null);
						}
						occurrenceAnnos = null;
					}
				}
			}
		}
		finally {
			super.handlePreferenceStoreChanged(event);
		}
	}

	/**
	 * Combine Twelf editor-specific preferences with preferences
	 * from org.eclipse.ui.editors.text and the platform prefs.
	 * Inspired by {@link JavaEditor#createCombinedPreferenceStore()},
	 * but removed currently unused IEditorInput argument.
	 * @return Combined preference store
	 */
	private IPreferenceStore createCombinedPreferenceStore() {
		List<IPreferenceStore> stores= new ArrayList<IPreferenceStore>(3);

		stores.add(TwelfActivator.getDefault().getPreferenceStore());
		stores.add(EditorsUI.getPreferenceStore());
		stores.add(PlatformUI.getPreferenceStore());

		return new ChainedPreferenceStore(stores.toArray(new IPreferenceStore[stores.size()]));
	}

	private void doSelectionChanged(ITextSelection sel) {
		if (sel.getOffset() < 0)
			return;
		IDocument doc = getSourceViewer().getDocument();
		IRegion id = TwelfModel.getModel().findTwelfWord(doc, sel.getOffset());
		if (id == null || id.getLength() <= 0)
			// selection not on an identifier
			return;
		if (occurrenceMarkerJob != null)
			occurrenceMarkerJob.cancel();
		try {
			IElfFile file = getFile();
			ITwelfNamedElement selected = TwelfModel.getModel().getBinding(
					file, doc, id);
			if (selected == null)
				return;

			OccurrencesFinder finder = 
				new OccurrencesFinder(selected, doc);
			if (selected instanceof ITwelfConstant) {
				List<ITwelfDeclaration> decls = TwelfAST.parseElfCode(doc.get());
				for (ITwelfDeclaration decl : decls)
					decl.dispatch(finder);
			}
			else {
				// this is a variable inside a declaration
				// optimization: only search the surrounding declaration
				ITwelfDeclaration decl = 
					TwelfModel.getModel().findDeclaration(file, doc, id.getOffset());
				decl.dispatch(finder);
			}
			
			if (! finder.getResult().isEmpty()) {
				// not sure if it's really all that useful to go through a job here
				// but the Java editor does it so we'll do it too
				// Java editor also cancels the job when document is changed...
				occurrenceMarkerJob = new OccurrenceMarkerJob(finder.getResult());
				occurrenceMarkerJob.run(new NullProgressMonitor());
			}
		} catch (RecognitionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (TokenStreamException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadLocationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Returns the lock object for the given annotation model.
	 *
	 * @param annotationModel the annotation model
	 * @return the annotation model's lock object
	 * @since 3.0
	 */
	private Object getLockObject(IAnnotationModel annotationModel) {
		if (annotationModel instanceof ISynchronizable) {
			Object lock= ((ISynchronizable) annotationModel).getLockObject();
			if (lock != null)
				return lock;
		}
		return annotationModel;
	}

	public static IElfFile getEditedFile(ITextEditor editor) {
		// TODO use annotation model but also file so we can look up other files
		// editor.getDocumentProvider().getAnnotationModel(editor.getEditorInput())
		IFile file = null;
		if (editor.getEditorInput() instanceof IFileEditorInput) {
			file = ((IFileEditorInput) editor.getEditorInput()).getFile();
		}
		IAnnotationModel model = editor.getDocumentProvider().getAnnotationModel(
				editor.getEditorInput());
		return file == null ? TwelfModel.getModel().getFileModel(model) : 
			TwelfModel.getModel().getFileModel(model, file);
	}

}
